C#网络编程

与 Dotnet Core 交互

作者:陈广 日期:2018-11-20


上一篇文章,我们使用 Fiddler 抓取了一个最简单静态页面的报文进行分析,并介绍了 HTTP GET 方法。但如果想要了解其它 HTTP 方法,使用静态页面就无法满足了,继续深入下去,就需要建立动态页面了。这一系列文章本来就是讲 .NET Core 的,所以本文将使用 .NET Core 来创建动态页面以继续学习 HTTP 协议。

静态网页与动态网页的区别

早期的网页都是静态的,也就是说任何人在任何地方,打开一个页面,显示的内容都是一成不变的。后面出现了新的需求,最简单的例子,不同的用户登录同一个页面,需要显示不同的内容;早上登录希望显示早上好,下午登录希望显示下午好,这此都是静态页面无法做到的,所以就出现了动态网页。两者的区别画张图就清楚了。

首先进静态网页的请求过程:

图 1: 静态网页请求过程
  • 首先浏览器输入 URL,向服务器请求网页。
  • Web 服务器在收到浏览器发送的请求后,解析 URL,根据解析出的路径及文件名(一般后缀为 .html),在本地查找此文件。
  • Web 服务器查找到相应的文件后,根据文件的 HTML 内容生成响应报文并返回给浏览器。
  • 浏览器解析返回的 HTML 并显示。

接下来是动态网页的请求过程:

图 2: 动态网页请求过程
  • 首先浏览器输入 URL,向服务器请求网页。
  • Web 服务器在收到浏览器发送的请求后,转发给服务程序(本文为 .NET Core 编写的 ASP.NET 应用)。
  • 服务程序在收到请求后,根据请求的不同参数,生成不同的 HTML,实际上就是实时拼凑出一段 HTML 代码,并返回给 Web 服务器。
  • Web 服务器将 HTML 代码包装成响应报文,返回给浏览器。
  • 浏览器解析返回的 HTML 并显示。

可以看到,在静态网页请求中,HTML 代码是写死在文件中的,使用时传给客户端。而在动态网页申请中,Web 服务器的角色发生了转变,它成为了代理,负责将请求转发给 Dotnet 服务程序,由服务程序对请求进行处理,并根据请求实时生成 HTML 传回给 Web 服务器。

使用浏览器中的开发者工具查看报文

接下来我们写一个 ASP.NET Core 应用程序,以观察动态网页所传送的报文。

编写一个简单的 ASP.NET Core MVC 应用

本例使用 .NET Coer 2.1 编写。新建一个名为 Demo 的文件夹,在文件夹上鼠标右键菜单选择【Open with Code】,使用 Visual Studio Code 打开此文件夹(在安装 vscode 结束时弹出的窗口上需要选择所有项,此项才会出现在右键菜单上)。

在 vscode 中按【Ctrl + ~】快捷键打开终端(应为波浪号下面那个符号,由于 Markdown 里此符号有别用,无法正常显示,故用波浪号代替)。输入dotnet new empty新建一个空 web 应用程序。

首先关掉 SSL,先不使用 https。打开 Properties 文件夹下的 launchSettings.json 文件。将iisSettings下的sslPort项的值改为0。然后删除demo下的applicationUrl项中的 https 项。最终代码如下所示(注意端口号可能有所不同):

{
  "iisSettings": {
    "windowsAuthentication": false, 
    "anonymousAuthentication": true, 
    "iisExpress": {
      "applicationUrl": "http://localhost:19077",
      "sslPort": 0
    }
  },
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    },
    "demo": {
      "commandName": "Project",
      "launchBrowser": true,
      "applicationUrl": "http://localhost:5000",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}

更改 Startup.cs 代码如下:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace demo
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvcWithDefaultRoute();
        }
    }
}

接下来,新建一个 Controllers 文件夹,并向其添加了个名为 HomeController.cs 的文件,输入如下代码:

using Microsoft.AspNetCore.Mvc;

namespace demo.Controllers
{
    public class HomeController : Controller
    {
        public string Index()
        {
            return "Hello World!";
        }
    }
}

此控制器非常简单,仅返回一个字符串,其实和静态网页没啥两样。

使用浏览器的开发者工具

本来还是想用 Fiddler 来抓包的,但很遗憾,vscode 和 vs 自带的 IIS 都是仅供本地使用的简易 web 服务器,无法抓包,也懒得想办法解决了。突然想起来 浏览器也可以查看报文。所以干脆以后就使用浏览器来查看报文吧,方便很多,不会象 Fiddler 一样会受到大量垃圾连接的干扰。这段双 11,电脑每时每刻都会有大量垃圾弹窗,痛苦!

保存完所有文件后,选择 vscode 的【Debug】菜单中的【Start Without Debugging】,此时启动浏览器,并显示Hello World!字样。我使用的是 Edge 浏览器,以后以此浏览器为例进行讲解。接下来在浏览器中按【F12】打开开发者工具,切换到【网络】选项卡,点击【http://localhost:5000】项查看报文,如下图所示:

图 3: 使用 Edge 查看报文

在 Edge 中,首部被称为标头,你记住它是 header 就可以了,我在文章中一般都使用英文。在浏览器中无法查看报文的完整原文,都是分好类以方便查看的。可以在【http://localhost:5000】上单击鼠标右键,选择不同的项以将原文复制至剪贴板。

接下来点击【正文】选项卡,可以查看响应 Body(Edge 将主体称为正文,你记住它是 Body 就行了)。如下图所示:

图 4: 使用查看 Body

在控制器中读取请求报文

控制器的基类Controller中有一个 Request 属性,可用于读取请求报文,下表是它的常用属性:

名称 描述
Path 此属性返回请求 URL 的路径部分
QueryString 此属性返回请求 URL 的查询字符串部分
Headers 此属性返回请求 header 的字典,按名称索引
Body 此属性返回可用于读取请求 body 的流
Form 此属性返回按名称索引的请求中表单数据的字典
Cookies 此属性返回按名称索引的请求 cookies 的字典

下面我们尝试读取请求报文中的 Header。更改 HomeController.cs 文件代码如下:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using System.Collections.Generic;

namespace demo.Controllers
{
    public class HomeController : Controller
    {
        public string Index()
        {
            string result = "";
            KeyValuePair<string, StringValues>[] headers = new KeyValuePair<string, StringValues>[Request.Headers.Count];

            Request.Headers.CopyTo(headers, 0);
            //用于读取Headers中的每个键值对
            foreach (KeyValuePair<string, StringValues> pair in headers)
            {
                result += pair.Key + ":";
                //每个值有可能包含多个字符串,以下是循环读取
                foreach (string s in pair.Value.ToArray())
                {
                    result += pair.Value[0] + "——";
                }
                result += "\r\n";
            }

            return result;
        }
    }
}

代码比较复杂,Request.Headers属性用于读取报文中的 header,它由一个键值对集合组成,而此集合中的每个项中的值又是一个集合,这是因为同一的 header 可能有多个值。所以读起来比较麻烦。

运行程序,浏览器中显示如下结果:

Connection:Keep-Alive——
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8——
Accept-Encoding:gzip, deflate——
Accept-Language:zh-CN——
Host:localhost:5000——
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134——
Upgrade-Insecure-Requests:1——

由结果可观察到,所有的值都只有一个元素,所以只需读取字符数组中的第一个值即可。另外,如果使用 LINQ,代码会简单一些。将 HomeController.cs 代码改为:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using System.Collections.Generic;
using System.Linq;

namespace demo.Controllers
{
    public class HomeController : Controller
    {
        public string Index()
        {
            string result = "";
            KeyValuePair<string, StringValues>[] headers = Request.Headers.ToArray();

            foreach (KeyValuePair<string, StringValues> pair in headers)
            {
                result += pair.Key + ":";
                result += pair.Value[0] + "\r\n";
            }

            return result;
        }
    }
}

运行结果为:

Connection:Keep-Alive
Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding:gzip, deflate
Accept-Language:zh-CN
Host:localhost:5000
User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134
Upgrade-Insecure-Requests:1

在控制器中生成响应报文

在 ASP.NET Core 中可以通过Controller基类的Response属性来手动的编写响应报文并返回。更改 HomeController 代码如下:

using Microsoft.AspNetCore.Mvc;
using System.Text;

namespace demo.Controllers
{
    public class HomeController : Controller
    {
        public void Index()
        {
            Response.StatusCode = 299; //指定状态码
            Response.ContentType = "text/html";
            //添加一个 header
            string[] values={"item 1","item 2"}; 
            Response.Headers.Add("Welcome",values);
            //指定主体
            byte[] content = Encoding.ASCII.GetBytes(
                $"<center><h1>Hello World!</h1></center>");
            Response.Body.WriteAsync(content, 0, content.Length);//返回响应
        }
    }
}

运行程序,返回结果如下图所示:

图 5: 手动生成响应

从图中可以看到,几项我手动更改的地方都使用红框圈了出来。上述代码指定了一个不存在的状态码,没有使用的 header,而且此 header 还有多个值。下图是响应 Body 的内容:

图 6: 手动生成响应中的 Body

这样写代码是非常糟糕的,写这个例子主要是演示 ASP.NET Core 在幕后是如何工作的,实际开发中千万不要这样使用。

浏览器向服务器传送数据

浏览器要向服务器传送数据,无非是通过请求报文进行传送,请求报文分为三个部分,所以数据可以放在这三个地方发送给服务器。

通过 URL 进行传送

请求报文的第一个部分就是 URL,简单数据可以直接通过 URL 进行传送。一般情况下是使用 URL 中的查询字符串向服务器传送数据,但现在更多时候是将数据放在 URL 段中。

使用 URL 段

以下 URL 地址中:

http://www.iotxfd.cn/home/index

其中/home/index为路径(path),路径中的homeindex则为 URL 中的两个段。一般情况下,第一个段home指向控制器,第二个段index则指向 action 方法。如果要传递数据,则应当使用更多的段。假设有如下 URL:

http://localhost:5000/home/index/valueOne/valueTwo

下面我们演示如何在 action 中获取valueOnevalueTwo两个值。首先要更改路由,加上两个自定义段变量,更改 Startup.cs 代码如下:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace demo
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc(routes=>
            {
                routes.MapRoute
                (
                    name:"default",
                    template:"{controller}/{action}/{v1}/{v2}"
                );
            });
        }
    }
}

这里,我们不再使用 MVC 的默认路由,而是自定义了一个路由。其中name参数指定路由的名称,template参数用于定义路由模式。在此路由中我们加入了两个自定义段变量v1v2,用于通过在 URL 中加入额外的段向服务器传递数据。接下来更改 action 方法,以接收这两个段变量。

接下来我们将 HomeController.cs 文件更改如下:

using Microsoft.AspNetCore.Mvc;

namespace demo.Controllers
{
    public class HomeController : Controller
    {
        public string Index(string v1,string v2)
        {
            return $"v1={v1}, v2={v2}";
        }
    }
}

在 Home 控制器中,我们给Index action 方法添加了两个参数v1v2,对应于路由模板中的v1v2段变量,此时,URL 中的第三和第四个段的值就会自动传给形参v1v2。运行程序,此时默认 URL 会返回一个 404 错误,我们在浏览器中输入如下 URL:

http://localhost:5000/home/index/valueOne/valueTwo

按回车后,浏览器中显示:

v1=valueOne, v2=valueTwo

两个值正好对应于我们在 URL 中输入的两个额外段。可以尝试更改 URL 中的第三和第四个段的值,然后按回车,查看浏览器显示内容的更改。

使用查询字符串

早期的程序更倾向于使用查询字符串来向服务器发送数据,上述两个额外的段如果改用查询字符串,则应更改如下:

http://localhost:5000/home/index?v1=valueOne&v2=valueTwo

?号表示后面的是查询字符串,多个查询字符串之间使用&分隔。下面我们将路由改为 MVC 的默认路由,更改 Startup.cs 文件如下:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;

namespace demo
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvcWithDefaultRoute();
        }
    }
}

Home 控制器代码无需更改,运行程序,浏览器启动时自动使用默认 URL,结果如下:

v1=, v2=

此时 URL 中并未使用查询字符串,所以v1v2的值为空。更改 URL 为:

http://localhost:5000/home/index?v1=valueOne&v2=valueTwo

然后按回车键,浏览器显示如下:

v1=valueOne, v2=valueTwo

可以看到,查询字符串中的两个值已经读取出来。

通过 Header 进行传送

一般情况下,不会通过 Header 来传送数据。浏览器通过 Header 向服务器传送用户数据一般情况下都是用在 Cookie 上。Cookie 我会在后面专门讲。但既然讲到这里,不妨演示下也是不错的选择。

我能查到的可以修改请求 Header 的方法只有一个,就是通过 JavaScript 中的XMLHttpRequest。而XMLHttpRequest是实现 Ajax 的核心对象。早期的 web 应用程序由于 HTTP 的无状态等原因,每次去服务器取数据时,连同整个页面的 HTML 等所有内容都要下载回本地,这实际上是浪费带宽资源,之后出现了 Ajax ,允许在不刷新页面的情况下下载数据,从而使浏览器的行为模式变得跟桌面应用程序更加类似。之后出现了 Vue、Angular、React 等 JavaScript 框架, 使得浏览器跟服务器之间只在第一次请求时传递标记需要传递 HTML,其它时候只需传递数据。它们所对应的服务器端则是 Web API。接下来我们演示如何使用 JavaScript 通过 Header 向服务器传递数据。

首先将 Index.cshtml 文件的代码更改如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    
</head>
<body>
    <button id="Button" onclick="Btn_Click()">发送</button>
    <div id="textBox"></div>
</body>
<script type="text/javascript">
        
    function Btn_Click(){
        var xhr = new XMLHttpRequest()
        var url = 'http://localhost:5000/home/header';
        xhr.open('GET',url);
        //设置 Header
        xhr.setRequestHeader("MyCustomerKey","this is my customer value"); 
        xhr.send(); //发送 Ajax 请求
        //当请求有数据返回时所调用的回调方法
        xhr.onreadystatechange=function(){
            var txtDiv = document.getElementById("textBox");
            if (xhr.readyState == 4 && xhr.status == 200) {   
                //将返回的数据添加到 div 中            
                txtDiv.innerText = xhr.responseText; 
            }else{
                //请求失败的处理
                txtDiv.innerText = "Fail";
            }
        }
    }
</script>
</html>

这里我在 Index 页面中安排了一个按钮,当点击按钮时,向服务器发送一个 Ajax 请求。此请求的响应使用异步实现,当有响应时会自动调用onreadystatechange属性所指定的方法。我在页面中安排了一个 div,用于显示从服务器中返回的数据。其中设置 Header 是通过XMLHttpRequestsetRequestHeader方法实现的。

接下来在服务器端提控制器中添加一个针对 Ajax 请求的 action。更改 HomeController.cs 代码如下:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using System.Collections.Generic;
using System.Linq;

namespace demo.Controllers
{
    public class HomeController : Controller
    {
        public ViewResult Index()
        {
            return View();
        }

        public string  Header()
        {
            string result = "";
            KeyValuePair<string, StringValues>[] headers = Request.Headers.ToArray();
            //读取请求 header,并将所有 header 加入到一个字符串内
            foreach (KeyValuePair<string, StringValues> pair in headers)
            {
                result += pair.Key + ":";
                result += pair.Value[0] + "\r\n";
            }
            //返回由请求 header 拼揍成的字符串
            return result;
        }

    }
}

这里我们声明了一个Header action 方法,它对应的是 Index.cshtml 文件中的 Ajax 请求的 URL:

http://localhost:5000/home/header

里面的代码很熟悉,借用了前面读取请求 header 的代码。最后 action 只是简单地向浏览器返回一个字符串。

最后,运行程序,在浏览器中按【F12】打开开发者工具,然后点击【发送按钮】,在开发者工具中选择【header】项,最终显示内容如下图所示:

图 7: Ajax 响应

查看请求 header,可以看到我们添加的自定义 Header。浏览器返回的内容中也可以看到自定义 Header 项。点击开发者工具的【正文】选项卡,可以发现,数据是通过 body 传递回浏览器的。

通过 Body 进行传送

之前我们使用的都是 GET 方法传递浏览器数据,如果需要使用 Body 进行数据传递,就需要使用 POST 方法了。这也是 GET 和 POST 方法的本质区别。浏览器需要通过表单来提交 POST 请求。

为演示 POST,我们将 Index.cshtml 代码更改如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    
</head>
<body>
    <form action="/Home/Create" method="POST">
        <input type="text" name="v1"/>
        <input type="text" name="v2"/>
        <input type="submit" value="提交"/>
    </form>
</body>
</html>

此页面只是简单地安排了两个input标签,用于输入传送到服务器的数据。form标签的action方法中我们指定了Create action,接下来更改 HomeController.cs 文件,以添加此 action。

using Microsoft.AspNetCore.Mvc;

namespace demo.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public string Create(string v1, string v2)
        {
            return $"v1={v1}\r\nv2={v2}";
        }
    }
}

运行程序,在自动打开的浏览器中按【F12】打开开发者工具,在浏览器显示的页面的文本框中分别输入两段文字,单击【提交】按钮,可以看到服务器把我们输入的文字返回给浏览器显示。如下图所示:

图 7: Post 数据

选择浏览器【开发者工具】中的使用【POST】方法的【Create】项,选择右边空格的【正文】和【请求正文】。可以看到,POST 请求的数据是通过 Body 进行传送的。

图 7:Post 数据通过 Body 进行传送

总结

本文主要介绍了浏览器与 ASP.NET Core 服务器之间的的数据传送方式以及读取方式。所使用都是非正规程序,但对于初学者来说,程序简单些,更有助于理解问题本质。要写正规代码,请参考我翻译的自由男大作《Pro ASP.NET Core MVC 2(第7版)》。

;

© 2018 - IOT小分队文章发布系统 v0.3